Analyse comparative des élections européennes 2019 et 2024 en France

Dynamiques géographiques et politiques

Auteur : Bernier Aude
Date : Octobre 2025
Source : Ministère de l’Intérieur — Données publiques


🧭 Contexte du projet

Les élections européennes constituent un baromètre important de l’évolution politique et territoriale en France. Entre 2019 et 2024, la scène politique française a connu des recompositions notables, marquées par des dynamiques différenciées selon les territoires : métropole, Antilles, océan Indien, etc.

Ce projet vise à analyser et visualiser ces évolutions à travers une approche géographique et politique, en s’appuyant sur des données électorales officielles.


❓ Problématique

Comment la géographie électorale française a-t-elle évolué entre les élections européennes de 2019 et 2024, et quelles dynamiques politiques et territoriales peut-on en dégager ?

Cette question guide l’ensemble de l’analyse. Elle implique de croiser des dimensions spatiales (par régions et départements) et politiques (par listes et familles partisanes).


🎯 Objectifs du projet

Objectif général : Comprendre les transformations du paysage électoral français entre 2019 et 2024 à travers une double approche.

🗺️ 1. Analyse géographique

  • Identifier les zones de stabilité et de basculement électoral.
  • Observer les contrastes entre régions métropolitaines et les DROM.
  • Mettre en évidence les dynamiques de participation (inscrits, exprimés, abstention).

🏛️ 2. Analyse politique

  • Étudier l’évolution du rapport de force entre les principales listes.
  • Visualiser les transferts ou progressions de vote entre 2019 et 2024.
  • Comprendre la recomposition du paysage partisan européen en France.

📊 Approche méthodologique

L’analyse repose sur :

  • Le traitement de données électorales par Python (pandas, geopandas, plotly)
  • Des visualisations interactives pour explorer les tendances régionales et nationales
  • Une mise en perspective politique des résultats observés

🧩 Structure du notebook

  1. Chargement et nettoyage des données
  2. Analyse géographique (cartes et indicateurs régionaux)
  3. Analyse politique (voix par liste, évolutions, classements)
  4. Conclusion : synthèse et perspectives

💡 Remarque : Ce notebook s’inscrit dans une démarche exploratoire. L’objectif n’est pas seulement de présenter des chiffres, mais d’interpréter les dynamiques électorales dans leur dimension territoriale et politique.

📂 Jeu de données

Ces jeux de données proviennent du site officiel des données du gouvernement français :

🌐 Datagouv | Données 2019 | Données 2024

Les identifiants ci-dessous correspondent aux différents fichiers récapitulant les résultats électoraux de chaque région et département. Ces identifiants ont été répartis dans deux dictionnaires afin de différencier les résultats de 2019 et de 2024.

💡 Astuce : Ces identifiants permettent d'accéder directement aux métadonnées via l’API dataset de data.gouv.fr.
In [88]:
# Dictionnaires identifiants -> nom_fichier
identifiants_2024 = {
    "2690a1ed-13fb-4164-a006-2878000bf4c1": "departement",
    '38e17714-1e07-46dc-96e1-71e752aa40d3': "region"
}

identifiants_2019 = {
    '4a26fcae-494b-4ef6-82bb-49fdd32c8159': "departement",
    'fd27d3c0-477d-4c87-ab38-8eb2543b5844': "region"
}
api_url = 'https://www.data.gouv.fr/api/1/datasets/r/'

📥 Chargement des données

Chargement des fichiers nécessaires via l’API dataset du gouvernement français.

✅ Automatisation : Les fichiers sont automatiquement convertis en DataFrame grâce à la librairie pandas.

Si besoin, les fichiers Excel des résultats des élections européennes sont également disponibles localement :

📁 Organisation des dossiers :
data/2019/ → résultats 2019
data/2024/ → résultats 2024

⚙️ Importations

Les bibliothèques essentielles sont importées pour le traitement, la manipulation et la visualisation des données électorales issues des scrutins européens.

📦 Librairies principales :
  • re — expressions régulières, nettoyage des chaînes
  • json — lecture et écriture de données structurées (API, GeoJSON)
  • unicodedata — normalisation et gestion des caractères accentués
  • numpy — calculs numériques et manipulation de tableaux
  • pandas — manipulation et transformation des données tabulaires
  • geopandas — gestion et visualisation de données géographiques
  • plotly.express — visualisations rapides et interactives
  • plotly.graph_objects — personnalisation fine des graphiques
  • plotly.subplots — création de figures multi-graphiques
In [89]:
# --- Importations ---
import re,json, unicodedata
import numpy as np
import pandas as pd
import geopandas as gpd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.io as pio
pio.renderers.default = "notebook_connected"

🧩 Fonction de chargement des fichiers

La fonction charger_fichiers() permet d’automatiser le téléchargement et le chargement des fichiers électoraux à partir des identifiants fournis par l’API dataset du gouvernement français.

⚙️ Principe :
  • Itère sur les identifiants et noms de fichiers fournis pour une année donnée.
  • Construit l’URL complète de téléchargement depuis l’API.
  • Charge chaque fichier au format Excel directement en DataFrame pandas.
  • Stocke le résultat dans un dictionnaire avec un nom clé combinant l’année et le type de fichier.
In [90]:
# --- Fonction chargement des fichiers ---
def charger_fichiers(identifiants, annee):
    dataframes = {}
    for identifiant, nom in identifiants.items():
        url = api_url + identifiant
        print(f"Chargement {annee} - {nom} ...")
        df = pd.read_excel(url)
        dataframes[f"{annee}_{nom}"] = df
    return dataframes
💡 Remarque : Cette approche garantit un chargement automatisé et homogène des données sources, facilitant la mise à jour et la réutilisation du notebook pour d’autres années électorales.

Chargement des tables 2024

Le chargement des DataFrames de l’année 2024 dure environ 3 minutes.

In [91]:
dfs_2024 = charger_fichiers(identifiants_2024, "2024")
Chargement 2024 - departement ...
Chargement 2024 - region ...

Chargement des tables 2019

Le chargement des DataFrames de l’année 2019 dure environ 1 minute.

In [92]:
dfs_2019 = charger_fichiers(identifiants_2019, "2019")
Chargement 2019 - departement ...
Chargement 2019 - region ...
💡 Remarque : Le temps de chargement dépend de la taille des fichiers et de la disponibilité de l’API data.gouv.fr. Les DataFrames obtenus sont stockés dans des dictionnaires indexés par année et nom de fichier.

🧮 Manipulation des données

Les données issues des élections européennes de 2019 présentent certaines incohérences structurelles, notamment :

  • Des colonnes non nommées (Unnamed) ou incomplètes.
  • Des valeurs inutiles ou redondantes dans plusieurs fichiers.

Afin de garantir la cohérence et la comparabilité des données entre 2019 et 2024, les algorithmes suivants effectuent un nettoyage automatique :

⚙️ Étapes principales :
  • Renommer toutes les colonnes Unnamed de manière explicite.
  • Supprimer les colonnes vides, les doublons ou les champs non pertinents.
  • Réorganiser la structure pour une uniformité entre les deux millésimes.

💡 Remarque : Ce prétraitement garantit une base de données propre et exploitable avant toute analyse géographique ou politique.

1. Initialisation des colonnes

Cette étape consiste à harmoniser la structure des colonnes entre les jeux de données 2019 et 2024. L’objectif est de rendre les noms de colonnes identiques afin de faciliter les opérations de comparaison et de concaténation.

⚙️ Principe :
  • Définir un mapping entre les noms de colonnes bruts et les noms harmonisés.
  • Renommer les colonnes dans chaque DataFrame selon ce dictionnaire de correspondance.
  • Uniformiser les structures afin d’éviter toute erreur lors de la fusion des millésimes.
In [93]:
# --- Définition des colonnes utiles et harmonisation des noms ---

# --- Colonnes utiles par niveau géographique ---
colonnes_utiles = {

    "departement": ["Code du département", "Libellé du département", "Code département", "Libellé département",
                    "Inscrits", "Votants", "Exprimés", "Abstentions", "Blancs", "Nuls"],
                    
    "region": ["Code de la région", "Libellé de la région", "Code région", "Libellé région",
               "Inscrits", "Votants", "Exprimés", "Abstentions", "Blancs", "Nuls"]
               }

# --- Mapping colonnes fixes harmonisées ---
mapping_colonnes = {
    # Codes Département
    "code_du_departement": "code_departement",
    "code_departement": "code_departement",

    # Codes Région
    "code_de_la_region": "code_region",
    "code_region": "code_region",

    # Libellés Département
    "libelle_du_departement": "nom_departement",
    "libelle_departement": "nom_departement",

    # Libellés Région
    "libelle_de_la_region": "nom_region",
    "libelle_region": "nom_region",

    # Listes électorales
    "libelle_abrege_de_liste": "liste_electorale",
    "libelle_abrege_liste": "liste_electorale",

    # Données électorales
    "inscrits": "inscrits",
    "votants": "votants",
    "exprimes": "exprimes",
    "exprimes_": "exprimes",
    "abstentions": "abstentions",
    "blancs": "blancs",
    "nuls": "nuls"
}
💡 Remarque : Cette étape garantit la compatibilité entre les deux années électorales et permet d’éviter les erreurs de fusion lors des comparaisons régionales ou nationales.

2. Normalisation et harmonisation des colonnes

Cette étape vise à rendre la structure des données totalement homogène entre les jeux 2019 et 2024. Deux fonctions principales sont mises en œuvre :

  • normaliser_nom_colonne() → uniformise la syntaxe des noms de colonnes (minuscules, suppression des accents, remplacement des espaces par underscores).
  • nettoyer_colonnes() → nettoie et restructure les fichiers pour ne garder que les colonnes pertinentes à l’analyse électorale.
In [94]:
# --- Normalisation générique ---
def normaliser_nom_colonne(col):
    """
    Normalise un nom de colonne :
    - string
    - minuscules
    - suppression des accents
    - espaces et ponctuations -> underscore
    """
    col = str(col)
    col = ''.join(c for c in unicodedata.normalize('NFD', col)
                  if unicodedata.category(c) != 'Mn')  # enlever accents
    col = col.lower()
    col = re.sub(r'[^a-z0-9]+', '_', col).strip('_')   # tout sauf alphanum -> _
    return col

# Construire une version normalisée une seule fois
MAPPING_NORM = { normaliser_nom_colonne(k): v for k, v in mapping_colonnes.items() }
In [95]:
# --- Nettoyage des données ---
def nettoyer_colonnes(df, niveau, colonnes_utiles):
    """
    Version optimisée mémoire :
    - Gère 2019 (avec 'Unnamed') et 2024 (sans)
    - Ne crée pas de colonnes drop inutiles
    - Aligne liste_electorale_i / voix_i
    - Normalise et mappe les noms de colonnes
    """
    def startswith_percent(name: str) -> bool:
        s = str(name).lstrip()
        return s.startswith("%") or s.startswith("%") or s.startswith("‰")

    dfcopy = df  # évite la copie inutile
    compteur = 1

    # --- Cas 2019 : blocs 'Unnamed' ---
    if any("Unnamed" in str(c) for c in dfcopy.columns):
        cols = list(dfcopy.columns)
        rename_map = {}
        bloc_size = 7  # taille typique observée
        start_bloc = None
        compteur = 1

        for idx, col in enumerate(cols):
            if "Unnamed" in str(col):
                if start_bloc is None:
                    start_bloc = idx
                pos = ((idx - start_bloc) % bloc_size) + 1
                if pos == 2:
                    rename_map[col] = f"liste_electorale_{compteur}"
                elif pos == 5:
                    rename_map[col] = f"voix_{compteur}"
                    compteur += 1
                else:
                    # colonnes inutiles : on ne les garde pas
                    continue
            else:
                rename_map[col] = col  # colonne fixe

        # On ne garde que les colonnes renommées existantes
        dfcopy = dfcopy.rename(columns=rename_map)
        dfcopy = dfcopy.loc[:, list(rename_map.values())]


    # --- Cas 2024 : colonnes explicites ---
    else:
        cols = list(dfcopy.columns)
        rename_map = {}
        compteur = 1
        i = 0
        bloc_size = 3  # hypothèse : 3 colonnes "liste" pour 1 "voix"

        while i < len(cols):
            col = cols[i]
            col_lower = str(col).lower()

            # Début d'un bloc : colonne contenant "liste"
            if "liste" in col_lower and not startswith_percent(col):
                # On regroupe les 3 prochaines colonnes "liste"
                bloc_listes = []
                for j in range(i, min(i + bloc_size, len(cols))):
                    if "liste" in str(cols[j]).lower() and not startswith_percent(cols[j]):
                        bloc_listes.append(cols[j])

                # On cherche la prochaine colonne "voix"
                j = i + bloc_size
                while j < len(cols) and "voix" not in str(cols[j]).lower():
                    j += 1

                # Renommer la 1ère colonne du bloc en liste_electorale_i
                if bloc_listes:
                    rename_map[bloc_listes[0]] = f"liste_electorale_{compteur}"

                # Si une colonne "voix" existe, on la mappe aussi
                if j < len(cols):
                    rename_map[cols[j]] = f"voix_{compteur}"
                    compteur += 1
                    i = j + 1
                else:
                    i += 1
            else:
                i += 1

        # Appliquer le mapping
        dfcopy = dfcopy.rename(columns=rename_map)

    # --- Normalisation et mapping ---
    rename_map = {}
    for c in dfcopy.columns:
        if startswith_percent(c):
            continue
        norm = normaliser_nom_colonne(c)
        rename_map[c] = MAPPING_NORM.get(norm, norm)
    dfcopy = dfcopy.rename(columns=rename_map)

    # --- Colonnes fixes ---
    colonnes_fixes = [
        MAPPING_NORM.get(normaliser_nom_colonne(x), normaliser_nom_colonne(x))
        for x in colonnes_utiles[niveau]
    ]

    # --- Colonnes finales à garder ---
    colonnes_a_garder = [
        c for c in dfcopy.columns
        if (
            c in colonnes_fixes
            or re.fullmatch(r"liste_electorale_\d+", c)
            or re.fullmatch(r"voix_\d+", c)
        )
        and not c.startswith("pct_")
    ]

    # --- Alignement vérification ---
    n_listes = len([c for c in colonnes_a_garder if c.startswith("liste_electorale_")])
    n_voix = len([c for c in colonnes_a_garder if c.startswith("voix_")])
    if n_listes != n_voix:
        print(f"⚠️ {niveau}: désalignement détecté ({n_listes} listes / {n_voix} voix")

    # --- Retour compact ---
    df_final = dfcopy.loc[:, colonnes_a_garder].copy(deep=False)
    return df_final
💡 Remarque : Ces deux fonctions assurent une normalisation complète et robuste des structures de données. Elles rendent les DataFrames exploitables pour les comparaisons entre millésimes, tout en préservant les informations essentielles pour l’analyse géographique et politique.

3. Données nettoyées

Les jeux de données 2019 et 2024 sont désormais harmonisés grâce aux fonctions de nettoyage et de normalisation précédemment définies. Cette étape applique la fonction nettoyer_colonnes() à l’ensemble des DataFrames et crée deux dictionnaires distincts : dfs_2019_clean et dfs_2024_clean.

⚙️ Objectif :
  • Nettoyer et structurer les données pour chaque niveau géographique (région, département).
  • Aligner les noms de colonnes et le format des valeurs entre 2019 et 2024.
  • Préparer les DataFrames pour les analyses comparatives et visuelles à venir.

💻 Code :

Vérification :

dfs_2019_clean["2019_region"].head()

dfs_2024_clean["2024_region"].head()

In [96]:
# --- Dataframes nettoyées ---
dfs_2019_clean = {}
dfs_2024_clean = {}

# Mapping des niveaux pour correspondre aux clés de colonnes_utiles
map_niveaux = {
    "region": "region",
    "departement": "departement"
}

for nom, df in dfs_2019.items():
    niveau_brut = nom.split("_")[1]
    niveau = map_niveaux.get(niveau_brut, niveau_brut)
    dfs_2019_clean[nom] = nettoyer_colonnes(df, niveau, colonnes_utiles)

for nom, df in dfs_2024.items():
    niveau_brut = nom.split("_")[1]
    niveau = map_niveaux.get(niveau_brut, niveau_brut)
    dfs_2024_clean[nom] = nettoyer_colonnes(df, niveau, colonnes_utiles)

#dfs_2019_clean["2019_region"].head()
#dfs_2024_clean["2024_region"].head()
💡 Remarque : Les dictionnaires dfs_2019_clean et dfs_2024_clean contiennent les versions nettoyées des données, prêtes pour les traitements analytiques, les agrégations et la construction des visualisations géographiques.

Jointures des bases de données

Cette étape permet de fusionner les jeux de données 2019 et 2024 afin de faciliter les comparaisons et les analyses visuelles. Les jointures sont réalisées sur les colonnes géographiques et électorales communes (code région, code département, etc.).

⚙️ Objectif :
  • Assembler les données de 2019 et 2024 dans une structure unifiée.
  • Assurer la correspondance parfaite des entités géographiques.
  • Préparer les DataFrames fusionnés pour les analyses graphiques et comparatives.

Mapping : abréviations et couleurs des listes électorales

Avant de procéder aux visualisations, un mapping est défini pour associer chaque liste électorale à :

  • une abréviation courte (ex. RN, RE, LFI...) ;
  • une couleur distinctive utilisée dans les graphiques pour assurer la lisibilité.
In [97]:
# --- Abréviations des listes & associations des listes par couleur ---
ABREVIATIONS_LISTES = {
    # --- Gauche ---
    "LA FRANCE INSOUMISE": "LFI",
    "UNION POPULAIRE": "LFI",
    "PARTI COMMUNISTE FRANÇAIS": "PCF",
    "LUTTE OUVRIÈRE": "LO",
    "PARTI SOCIALISTE": "PS",
    "PLACE PUBLIQUE": "PS",
    "NOUVELLE DONNE": "PS",
    "EUROPE ÉCOLOGIE": "EELV",
    "LES ÉCOLOGISTES": "EELV",
    "GÉNÉRATION.S": "EELV",
    "GAUCHE RÉPUBLICAINE ET SOCIALISTE": "GRS",
    "PARTI RADICAL DE GAUCHE": "PRG",
    "ENVIE D'EUROPE": "EEU",
    "URGENCE ÉCOLOGIE": "UECO",
    "À VOIX ÉGALES": "AVE",
    "RÉVOLUTIONNAIRE": "REVG",
    "RÉVOLUTION CITOYENNE": "RCIT",

    # --- Centre / Majorité présidentielle ---
    "RENAISSANCE": "RE",
    "LA RÉPUBLIQUE EN MARCHE": "LREM",
    "MOUVEMENT DÉMOCRATE": "MODEM",
    "HORIZONS": "HOR",
    "MAJORITÉ PRÉSIDENTIELLE": "RE",
    "ENSEMBLE": "RE",
    "ALLIANCE CENTRISTE": "RE",
    "LES EUROPÉENS": "LEU",
    "PARTI DES CITOYENS EUROPÉENS": "PCE",
    "PARTI FÉDÉRALISTE EUROPÉEN": "PFE",
    "PACE": "PACE",
    "POUR L'EUROPE DES GENS": "PEG",
    "UDLEF": "UDLEF",
    "LENS": "RE",   # Ensemble majorité présidentielle

    # --- Droite ---
    "LES RÉPUBLICAINS": "LR",
    "LLR": "LR",
    "UNION DROITE-CENTRE": "UDC",
    "DEBOUT LA FRANCE": "DLF",
    "LES PATRIOTES": "PAT",
    "UNION DES DÉMOCRATES ET INDÉPENDANTS": "UDI",
    "UNE FRANCE ROYALE": "UFR",
    "LISTE DE LA RECONQUÊTE": "REC",
    "LREC": "REC",

    # --- Extrême droite ---
    "RASSEMBLEMENT NATIONAL": "RN",
    "LRN": "RN",
    "FRONT NATIONAL": "RN",
    "RECONQUÊTE": "REC",
    "RECONQUÊTE !": "REC",
    "PRENEZ LE POUVOIR": "RN",

    # --- Autres / Divers ---
    "PARTI ANIMALISTE": "ANI",
    "ALLIANCE RURALE": "AR",
    "LES OUBLIES DE L'EUROPE": "OUBL",
    "LISTE CITOYENNE": "LCIT",
    "NEUTRE ET ACTIF": "NEUT",
    "LEXD": "EXD",   # extrême droite
    "LEXG": "EXG",   # extrême gauche
    "LVEC": "EELV",  # variante écologiste
    "LUG": "PS",     # variante gauche unie
    "LECO": "EELV",
    "LCOM": "PCF",
    "DIV": "DIV",
    "LDIV": "DIV",   # Divers
    "AUT": "AUT",
    "LDVD": "DVD",   # Divers droite
    "LDVG": "DVG",   # Divers gauche
}

# Palette de couleurs pour chaque sigle de parti
COULEURS_LISTES = {
    # --- Gauche / Extrême gauche ---
    "LFI": "#B2182B",   # Rouge foncé (La France Insoumise)
    "PCF": "#D73027",   # Rouge vif (Communistes)
    "LO": "#EF8A62",    # Rouge clair (Lutte Ouvrière)
    "PS": "#F781BF",    # Rose (Parti Socialiste)
    "LUG": "#F781BF",   # Gauche unie
    "EELV": "#4DAF4A",  # Vert (Écologistes)
    "LVEC": "#4DAF4A",
    "GRS": "#A6D96A",   # Vert clair
    "PRG": "#E41A1C",   # Rouge clair
    "DEC": "#C7E9C0",   # Vert pâle (Décroissance)
    "UECO": "#66C2A5",  # Urgence écologie
    "AVE": "#D9F0D3",   # À voix égales
    "REVG": "#FB8072",  # Révolutionnaire
    "RCIT": "#FBB4AE",  # Révolution citoyenne
    "EXG": "#FB9A99",   # Extrême gauche
    "DVG": "#F4A582",   # Divers gauche (rose saumon)

    # --- Centre / Majorité présidentielle ---
    "RE": "#FFD92F",    # Jaune (Renaissance)
    "LREM": "#FFD92F",
    "HOR": "#E6AB02",   # Jaune-orangé (Horizons)
    "MODEM": "#FDB863", # Orange clair
    "ENS": "#E6AB02",
    "LEU": "#FFFFB3",
    "PACE": "#FFED6F",
    "PCE": "#FEE391",
    "PFE": "#FEE08B",
    "UDLEF": "#FEE08B",
    "PEG": "#FEE08B",

    # --- Droite ---
    "LR": "#377EB8",    # Bleu (LR)
    "LLR": "#377EB8",
    "UDI": "#80B1D3",   # Bleu clair (UDI)
    "DLF": "#0571B0",   # Bleu moyen (Debout la France)
    "UDC": "#6BAED6",   # Union Droite-Centre
    "PAT": "#08519C",   # Patriotes
    "UFR": "#08306B",   # France Royale (bleu royal)
    "DVD": "#2171B5",   # Divers droite (bleu acier)
    "EXD": "#08306B",   # Extrême droite modérée

    # --- Extrême droite ---
    "RN": "#08306B",    # Bleu très foncé (Rassemblement National)
    "REC": "#542788",   # Violet foncé (Reconquête)
    "LREC": "#542788",
    "FREXIT": "#7B3294",

    # --- Citoyens / Divers / Autres ---
    "ANI": "#A6761D",   # Brun (Animaliste)
    "AR": "#B15928",    # Brun rougeâtre (Alliance Rurale)
    "OUBL": "#969696",  # Gris moyen (Les Oubliés)
    "LCIT": "#CCCCCC",  # Liste Citoyenne
    "NEUT": "#D9D9D9",  # Neutre et Actif
    "AJ": "#BDBDBD",    # Alliance Jaune
    "AE": "#BDBDBD",    # Allons Enfants
    "ESPPL": "#CAB2D6", # Europe au service des peuples
    "ESP": "#CAB2D6",
    "IC": "#E0E0E0",    # Initiative Citoyenne
    "LC": "#CCCCCC",
    "PEU": "#B3B3B3",
    "PIR": "#984EA3",   # Parti Pirate
    "PRC": "#FB8072",
    "LFC": "#B2DF8A",   # La France Citoyenne
    "AUT": "#E0E0E0",
    "DIV": "#D9D9D9",
}
💡 Remarque : Ce mapping garantit une cohérence visuelle sur l’ensemble des graphiques, et facilite l’identification rapide des partis politiques à travers les visualisations comparatives 2019–2024.

1. Fonctions utilitaires

Ces fonctions centralisent les opérations récurrentes nécessaires à la manipulation, la transformation et la présentation des données. Elles seront utilisées tout au long du projet pour l’harmonisation des codes, la mise en forme des colonnes et la gestion des visualisations.

📦 Fonctions incluses :
  • harmoniser_codes() → unifie les codes géographiques (région, département, commune).
  • transformer_voix_listes() → convertit les données en format long pour l’analyse par liste électorale.
  • nettoyer_nom() → simplifie et normalise les noms (sans accents ni ponctuation).
  • nom_court() → génère les abréviations standardisées des listes électorales.
  • renommer_legende_auto() → reformate automatiquement les légendes et titres de colorbar des graphiques Plotly.
In [98]:
# --- Fonctions utilitaires ---
def harmoniser_codes(df):
    """
    Harmonise les colonnes de codes administratifs :
    - code_region
    - code_departement
    - code_commune
    
    Les convertit en string si elles existent dans le DataFrame.
    """
    colonnes_codes = [
        "code_region", "code_departement", "code_commune",
    ]
    
    for col in colonnes_codes:
        if col in df.columns:
            df[col] = df[col].astype(str)
    return df


def transformer_voix_listes(df, id_vars):
    """
    Met au format long (liste, voix) à partir des colonnes voix_i et liste_electorale_i.
    id_vars: str ou liste de colonnes identifiantes (ex: 'nom_region_2024' ou ['code_region','nom_region_2024'])
    """
    if isinstance(id_vars, str):
        id_vars = [id_vars]

    voix_cols = [c for c in df.columns if re.fullmatch(r"voix_\d+(_2019|_2024)?", c)]
    if not voix_cols:
        raise ValueError("Aucune colonne voix_i trouvée.")

    # --- construit mapping voix_i_YYYY -> nom de la liste stable ---
    mapping = {}
    for c in voix_cols:
        m = re.match(r"(voix_\d+)(_2019|_2024)?", c)
        base, suffix = m.group(1), (m.group(2) or "")
        idx = base.split("_")[1]
        col_liste = f"liste_electorale_{idx}{suffix}"

        if col_liste in df.columns:
            # Cherche la valeur la plus fréquente (mode) pour ce nom de liste
            lib = df[col_liste].dropna().mode()
            mapping[c] = lib.iloc[0] if not lib.empty else base
        else:
            mapping[c] = base  # fallback

    # --- passage au format long ---
    df_melt = df.melt(
        id_vars=id_vars,
        value_vars=voix_cols,
        var_name="col_voix",
        value_name="voix"
    )

    # --- associe la liste électorale ---
    df_melt["liste"] = df_melt["col_voix"].map(mapping)

    # --- extrait l’année ---
    df_melt["annee"] = df_melt["col_voix"].str.extract(r"_(2019|2024)$")

    return df_melt

def nettoyer_nom(nom):
    """Supprime accents, ponctuation et met en minuscules pour normaliser les comparaisons."""
    if not isinstance(nom, str):
        return ""
    # enlever accents
    nom = unicodedata.normalize("NFKD", nom).encode("ascii", errors="ignore").decode("utf-8")
    # supprimer ponctuation et caractères spéciaux
    nom = re.sub(r"[^a-z0-9 ]", " ", nom.lower())
    # nettoyer les espaces multiples
    return re.sub(r"\s+", " ", nom).strip()

def nom_court(nom_liste, n=22):
    """Retourne le sigle standardisé d'une liste électorale, avec correspondance robuste."""
    nom_norm = nettoyer_nom(nom_liste)

    for cle, abbr in ABREVIATIONS_LISTES.items():
        if nettoyer_nom(cle) in nom_norm:
            return abbr

    # sinon, renvoie une version courte du nom (utile pour micro-listes)
    return str(nom_liste)[:n]

def renommer_legende_auto(fig):
    """
    Renomme automatiquement les légendes et les colorbars dans une figure Plotly Express ou une figure combinée.
    Compatible avec :
      - Graphiques multi-traces (bar, line, etc.)
      - Choropleths simples
      - Choropleths en sous-graphes (make_subplots)
    """
    def format_label(name):
        if not isinstance(name, str):
            return name
        base = re.sub(r"[_]+", " ", name).strip().capitalize()

        # Détecte les années à la fin
        match = re.match(r"(.*?)(\d{4})$", base)
        if match:
            texte, annee = match.groups()
            texte = texte.strip().lower()
            # Améliore la lisibilité avec "de" ou "d’"
            if texte.startswith("taux "):
                if texte.startswith("taux a"):  # abstention
                    texte = "Taux d’abstention"
                elif texte.startswith("taux e"):  # exprimés
                    texte = "Taux d’exprimés"
                elif texte.startswith("taux p"):  # participation
                    texte = "Taux de participation"
                else:
                    texte = "Taux de " + texte.split(" ", 1)[-1]
            else:
                texte = texte.capitalize()
            return f"{texte} : {annee}"
        return base.capitalize()

    # --- Cas 1 : Graphiques multi-traces (barres, lignes, etc.)
    if len(fig.data) > 1:
        fig.for_each_trace(lambda t: t.update(name=format_label(t.name)))

    # --- Cas 2 : Graphiques à 1 seule trace avec colorbar (choropleth simple)
    elif len(fig.data) == 1 and hasattr(fig.data[0], "colorbar"):
        trace = fig.data[0]
        if trace.colorbar.title.text:
            fig.update_coloraxes(colorbar_title_text=format_label(trace.colorbar.title.text))
        else:
            fig.update_coloraxes(colorbar_title_text=format_label(trace.name))

    # --- Cas 3 : Sous-graphiques avec plusieurs choropleths
    else:
        for trace in fig.data:
            if hasattr(trace, "colorbar") and trace.colorbar.title.text:
                fig.update_traces(
                    selector={"coloraxis": trace.coloraxis},
                    colorbar_title_text=format_label(trace.colorbar.title.text)
                )
    return fig
💡 Remarque : Ces fonctions constituent la boîte à outils centrale du projet : elles assurent une gestion uniforme des noms, des formats et des légendes, garantissant une cohérence complète entre la structure des données et les visualisations.

2. Jointures 2019 ↔ 2024 + rattachements hiérarchiques

Cette étape combine les jeux de données 2019 et 2024 afin de permettre les comparaisons temporelles.

Les opérations consistent à harmoniser les codes administratifs, renommer les colonnes pour chaque année et effectuer les jointures entre bases régionales et départementales.

⚙️ Objectif :
  • Uniformiser les clés géographiques (code_region,code_departement).
  • Ajouter des suffixes _2019 et _2024 pour distinguer les variables.
  • Réaliser les jointures pour obtenir des structures comparables par entité géographique.
In [99]:
# --- Chargement & manipulation des bases nettoyées + jointures 2019-2024 ---

# --- Sélection des niveaux géographiques
df_reg_2019 = dfs_2019_clean["2019_region"]
df_reg_2024 = dfs_2024_clean["2024_region"]

df_dep_2019 = dfs_2019_clean["2019_departement"]
df_dep_2024 = dfs_2024_clean["2024_departement"]

# --- Harmoniser les codes administratifs avant jointures
df_reg_2019 = harmoniser_codes(df_reg_2019)
df_reg_2024 = harmoniser_codes(df_reg_2024)

df_dep_2019 = harmoniser_codes(df_dep_2019)
df_dep_2024 = harmoniser_codes(df_dep_2024)

# --- Ajouter un suffixe "_2024" à toutes les colonnes (sauf la clé)

df_reg_2024 = df_reg_2024.rename(columns={
    c: f"{c}_2024" for c in df_reg_2024.columns if c != "code_region"})

# Idem pour 2019 si besoin
df_reg_2019 = df_reg_2019.rename(columns={
    c: f"{c}_2019" for c in df_reg_2019.columns if c != "code_region"})

df_dep_2024 = df_dep_2024.rename(columns={
    c: f"{c}_2024" for c in df_dep_2024.columns if c != "code_departement"})

# Idem pour 2019 si besoin
df_dep_2019 = df_dep_2019.rename(columns={
    c: f"{c}_2019" for c in df_dep_2019.columns if c != "code_departement"})

# --- Jointure pour effectuer des comparaisons entre 2019 et 2024

# Région
df_reg = df_reg_2019.merge(
    df_reg_2024,
    on="code_region",
    suffixes=("_2019", "_2024"))

# Département
df_dep = df_dep_2019.merge(
    df_dep_2024,
    on="code_departement",
    suffixes=("_2019", "_2024"))

# df_reg.head()
# df_dep.head()
💡 Remarque : Les jeux de données df_reg et df_dep obtenus contiennent désormais toutes les informations nécessaires pour les comparaisons régionales et départementales entre 2019 et 2024, constituant la base des analyses graphiques suivantes.

3. Géométries (GeoJSON) & fusion spatiale

Les jeux de données géographiques sont importés à partir des fichiers GeoJSON disponibles sur le dépôt FranceGEOJSON. Cette étape permet de rattacher les informations électorales aux géométries régionales et départementales, pour produire les futures cartes interactives.

⚙️ Objectif :
  • Importer les fichiers géographiques des régions et départements.
  • Harmoniser les codes administratifs entre les DataFrames et les GeoDataFrames.
  • Effectuer la fusion spatiale pour relier les géométries aux données électorales.
In [100]:
# --- URLs GEOJSON & merges des géométries

# --- URLs FranceGEOJSON (régions + départements avec DROM)
url_regions = "https://france-geojson.gregoiredavid.fr/repo/regions.geojson"
url_deps    = "https://france-geojson.gregoiredavid.fr/repo/departements.geojson"

gdf_regions = gpd.read_file(url_regions)
gdf_deps    = gpd.read_file(url_deps)

# Harmoniser types des codes (string)
gdf_regions["code"] = gdf_regions["code"].astype(str)
df_reg["code_region"] = df_reg["code_region"].astype(str)

gdf_deps["code"] = gdf_deps["code"].astype(str)
df_dep["code_departement"] = df_dep["code_departement"].astype(str)

# --- Correction des codes DOM nécessaire
map_dom = {
    "01": "1", "02": "2", "03": "3",
    "04": "4", "06": "6"
}
gdf_regions["code"] = gdf_regions["code"].replace(map_dom)
# Merge avec géométries
gdf_reg_final = gdf_regions.merge(df_reg, left_on="code", right_on="code_region", how="left")
gdf_dep_final = gdf_deps.merge(df_dep, left_on="code", right_on="code_departement", how="left")

# Vérification des bases pour graphiques
print("Bases prêtes :")
print("Régions :", gdf_reg_final.shape)
print("Départements :", gdf_dep_final.shape)
Bases prêtes :
Régions : (18, 160)
Départements : (96, 160)
💡 Remarque : Les GeoDataFrames gdf_reg_final et gdf_dep_final constituent les fondations spatiales des visualisations. Elles permettront d’associer dynamiquement les résultats électoraux aux géométries pour la création de cartes régionales et départementales.

📊 Data Visualisation

1. Chargement et préparation des données

Cette première étape prépare les bases de données nettoyées et harmonisées pour la phase de visualisation. Elle consiste à copier les GeoDataFrames finaux et à calculer des indicateurs électoraux communs tels que les taux de participation et d’abstention pour 2019 et 2024.

⚙️ Objectif :
  • Charger les GeoDataFrames finaux (gdf_reg_final et gdf_dep_final).
  • Créer de nouveaux indicateurs : taux de participation et taux d’abstention.
  • Préparer les données pour l’affichage graphique (cartes et bar charts comparatifs).
In [101]:
# --- Calculs des indicateurs ---

# Charger les bases nettoyées et harmonisées
df_reg = gdf_reg_final.copy()
df_dep = gdf_dep_final.copy()

# Calcul d'indicateurs communs
for df in [df_reg, df_dep]:
    df["taux_participation_2024"] = df["votants_2024"] / df["inscrits_2024"] * 100
    df["taux_participation_2019"] = df["votants_2019"] / df["inscrits_2019"] * 100
    df["taux_abstention_2024"] = 100 - df["taux_participation_2024"]
    df["taux_abstention_2019"] = 100 - df["taux_participation_2019"]
💡 Remarque : Ces indicateurs serviront à construire les premières visualisations comparatives, notamment sur les dynamiques de participation électorale entre 2019 et 2024, tant au niveau régional que départemental.

2. Analyse de la participation électorale

Cette section examine l’évolution du corps électoral et du taux de participation entre les élections européennes de 2019 et 2024. Elle met en évidence les dynamiques régionales et départementales à travers plusieurs visualisations interactives.

🎯 Objectif analytique :
  • Comparer le nombre d’inscrits et de suffrages exprimés entre 2019 et 2024.
  • Étudier l’évolution du taux de participation à différents niveaux géographiques.
  • Identifier les territoires où la participation électorale a le plus évolué.

Liste des graphiques réalisés

  1. Comparaison 2019 vs 2024 du corps électoral et des suffrages exprimés par région
    • Graphique en barres comparatives pour chaque région.
    • Permet d’observer la stabilité du nombre d’inscrits et l’évolution des exprimés.

  2. Évolution du taux de participation par région (2019 vs 2024)
    • Visualisation en barres verticales avec codes couleur par année.
    • Mise en évidence des hausses ou baisses de participation régionales.

  3. Taux de participation par département (2019 vs 2024)
    • Carte choroplèthe interactive (Plotly) illustrant les écarts de participation.
    • Possibilité de survoler les départements pour afficher les valeurs précises.

  4. Focalisation : taux de participation du département d’Île-de-France
    • Zoom spécifique sur la région francilienne.
    • Analyse du différentiel de participation entre 2019 et 2024.

💻 Étapes de réalisation :
  • Utilisation de plotly.express pour les comparaisons régionales (bar charts).
  • Exploration des taux via des cartes choropleth basées sur gdf_reg_final et gdf_dep_final.
  • Application de la fonction renommer_legende_auto() pour une présentation homogène.

💡 Remarque : Ces visualisations offrent une compréhension globale de la dynamique électorale française, mettant en lumière les contrastes entre les grandes régions métropolitaines et les départements d’outre-mer.

📊 Comparaison 2019 vs 2024 du corps électoral et des suffrages exprimés par région

Ce graphique compare, pour chaque région française, le nombre d’inscrits et le nombre de suffrages exprimés entre les élections européennes de 2019 et 2024. Il met en évidence les dynamiques de participation et la stabilité du corps électoral à l’échelle régionale.

🎯 Objectif :
  • Comparer le volume du corps électoral (inscrits) entre 2019 et 2024.
  • Visualiser la progression ou la baisse des suffrages exprimés selon les régions.
  • Identifier les zones de forte ou faible mobilisation électorale.
In [102]:
# --- Graphique : Corp électoral et suffrages exprimés par région ---

# --- Tri des régions
ordre_regions = (
    df_reg.groupby("nom_region_2024")["exprimes_2024"]
    .sum()
    .sort_values(ascending=False)
    .index.tolist()
)

# --- Figure
fig = go.Figure()

# --- Barres 2019 
# Exprimés (partie inférieure)
fig.add_trace(go.Bar(
    x=df_reg["nom_region_2024"],
    y=df_reg["exprimes_2019"],
    name="Exprimés 2019",
    marker_color="rgba(255,140,0,0.9)",
    offsetgroup="2019"
))
# Inscrits (complément par-dessus)
fig.add_trace(go.Bar(
    x=df_reg["nom_region_2024"],
    y=df_reg["inscrits_2019"] - df_reg["exprimes_2019"],
    base=df_reg["exprimes_2019"],
    name="Inscrits 2019",
    marker_color="rgba(100,149,237,0.4)",
    offsetgroup="2019"
))

# --- Barres 2024
# Exprimés (partie inférieure)
fig.add_trace(go.Bar(
    x=df_reg["nom_region_2024"],
    y=df_reg["exprimes_2024"],
    name="Exprimés 2024",
    marker_color="rgba(255,69,0,0.9)",
    offsetgroup="2024"
))
# Inscrits (complément par-dessus)
fig.add_trace(go.Bar(
    x=df_reg["nom_region_2024"],
    y=df_reg["inscrits_2024"] - df_reg["exprimes_2024"],
    base=df_reg["exprimes_2024"],
    name="Inscrits 2024",
    marker_color="rgba(30,144,255,0.4)",
    offsetgroup="2024"
))

# --- Mise en forme
fig.update_layout(
    barmode="group",
    bargap=0.25,
    title="Corps électoral et suffrages exprimés par région — 2019 vs 2024",
    xaxis=dict(
        categoryorder="array",
        categoryarray=ordre_regions,
        title="Région"
    ),
    yaxis_title="Nombre de personnes",
    legend_title="Catégorie",
    template="plotly_white"
)

fig.show()

Analyse du graphique : évolution de la participation électorale entre 2019 et 2024

Le graphique met en évidence une stabilité globale du nombre d’inscrits entre les élections européennes de 2019 et de 2024 : le corps électoral reste relativement constant dans l’ensemble des régions françaises.

En revanche, le nombre de suffrages exprimés augmente légèrement dans la majorité des régions, notamment dans les plus peuplées comme l’Île-de-France, Auvergne–Rhône-Alpes ou encore l’Occitanie, où la différence de participation entre les deux scrutins est visible. De manière générale, l’ensemble des régions métropolitaines témoignent d’une mobilisation électorale légèrement supérieure en 2024.

📊 Interprétation régionale :
  • Stabilité du corps électoral → peu de variation du nombre d’inscrits entre 2019 et 2024.
  • Légère hausse des exprimés dans la plupart des grandes régions métropolitaines.
  • Participation plus faible dans les DROM (Guadeloupe, Martinique, Guyane, Mayotte, La Réunion).

🔍 Focus : Les régions et départements d’outre-mer présentent une faible participation et leur rapport inscrits/exprimés est proportionnellement plus faible que celui observé en métropole. Leur poids démographique est moindre, la tendance de désengagement électoral y est similaire.

💬 Conclusion : Le graphique illustre que seule environ la moitié des électeurs inscrits participe effectivement au scrutin européen. Les barres orange (suffrages exprimés) atteignent à peine la moitié des barres bleues (inscrits), traduisant un taux d’abstention avoisinant 50 %. Cette tendance souligne un désintérêt persistant pour les élections européennes, sans distinction marquée entre métropole et outre-mer, ni entre les espaces urbains et ruraux.

📈 Évolution du taux de participation par région (2019 vs 2024)

Cette visualisation met en évidence les variations du taux de participation entre les élections européennes de 2019 et 2024 pour chaque région française. Elle permet d’identifier les territoires ayant connu une hausse ou une baisse notable de la mobilisation électorale.

🎯 Objectif :
  • Comparer les taux de participation régionaux entre 2019 et 2024.
  • Visualiser les écarts de mobilisation sur l’ensemble du territoire.
  • Repérer les régions les plus dynamiques électoralement.
In [103]:
# --- Définitions des zones géographiquess ---
codes_metropole = ["11","24","27","28","32","44","52","53","75","76","84","93","94"]
codes_antilles = ["1", "2", "3"]  # Guadeloupe, Martinique, Guyane
codes_indien = ["4", "6"]           # Réunion, Mayotte

# --- Initialisation des zones --- 
zones = [
    ("metropole", 1, {"lat": 46.6, "lon": 2.5}),
    ("antilles", 2, {"lat": 12, "lon": -61}),
    ("indien", 3, {"lat": -18, "lon": 52}),
]

def filtre_zone(gdf, zone, code):
    if zone == "metropole":
        return gdf[gdf[code].isin(codes_metropole)]
    elif zone == "antilles":
        return gdf[gdf[code].isin(codes_antilles)]
    elif zone == "indien":
        return gdf[gdf[code].isin(codes_indien)]
    else:
        return gdf
    
def assurer_CRS(df):
    """ S'assurer du CRS lon/lat (WGS84) + """

    if getattr(df, "crs", None) is not None:
        try:
            if df.crs.to_epsg() != 4326:
                df = df.to_crs(epsg=4326)
        except Exception:
            df = df.to_crs(epsg=4326)
            
    # ---  Identifiants stables & GeoJSON ---
    df = df.reset_index(drop=True)
    geojson = json.loads(df.to_json())

    return df, geojson

df = assurer_CRS(df_reg)[0]
geojson_reg = assurer_CRS(df_reg)[1]
In [104]:
# --- Graphique : Taux de participation par région ---

# --- Figure 3x2 ---
fig = make_subplots(
    rows=3, cols=2,
    subplot_titles=[
        "Métropole 2019", "Métropole 2024",
        "Antilles-Guyane 2019", "Antilles-Guyane 2024",
        "Océan Indien 2019", "Océan Indien 2024"
    ],
    specs=[
        [{"type": "choropleth"}, {"type": "choropleth"}]]*3,
    horizontal_spacing=0.03,
    vertical_spacing=0.07
)

# --- Boucle de génération des 6 cartes ---
for row, (zone, _, center) in enumerate(zones, start=1):
    for col, annee in enumerate(["2019", "2024"], start=1):
        gdf_zone = filtre_zone(df_reg, zone, "code")

        # Si aucune donnée (sécurité)
        if gdf_zone.empty:
            continue

        fig.add_trace(
            go.Choropleth(
                geojson=gdf_zone.__geo_interface__,
                locations=gdf_zone["code"],
                z=gdf_zone[f"taux_participation_{annee}"],
                featureidkey="properties.code",
                coloraxis="coloraxis",
                text=gdf_zone[f"nom_region_{annee}"],
                hovertemplate="<b>%{text}</b><br>Taux de participation: %{z:.1f}%<extra></extra>",
            ),
            row=row, col=col
        )

# --- Lier chaque carte à un “geo” indépendant ---
for idx, geo in enumerate(["geo1","geo2","geo3","geo4","geo5","geo6"]):
    fig.data[idx].geo = geo

# --- Mise en forme et centrage précis ---
fig.update_layout(
    dragmode=False,       # désactive le drag
    coloraxis=dict(
        colorscale="Viridis",
        colorbar=dict(title="Taux de participation (%)", len=0.75, y=0.4)
    ),
    
    # Métropole
    geo=dict(center={"lat": 46.6, "lon": 2.5},projection_type="mercator",lonaxis_range=[-6, 10],lataxis_range=[41, 52],
              showcoastlines=True,showcountries=True,fitbounds="locations"),
    geo2=dict(center={"lat": 46.6, "lon": 2.5},projection_type="mercator",lonaxis_range=[-6, 10],lataxis_range=[41, 52],
              showcoastlines=True,showcountries=True,fitbounds="locations"),
    # Antilles-Guyane
    geo3=dict(center={"lat": 12, "lon": -61},projection_type="mercator",lonaxis_range=[-67, -49],lataxis_range=[4, 18],
              showcoastlines=True,showcountries=True,fitbounds="locations"),

    geo4=dict(center={"lat": 12, "lon": -61},projection_type="mercator",lonaxis_range=[-67, -49],lataxis_range=[4, 18],
              showcoastlines=True,showcountries=True,fitbounds="locations"),
    # Océan Indien
    geo5=dict(center={"lat": -18, "lon": 52},projection_type="mercator",lonaxis_range=[43, 58], lataxis_range=[-23, -10],
              showcoastlines=True,showcountries=True,fitbounds="locations"),
    geo6=dict(center={"lat": -18, "lon": 52},projection_type="mercator",lonaxis_range=[43, 58], lataxis_range=[-23, -10],
              showcoastlines=True,showcountries=True,fitbounds="locations"),

    title_text="Taux de participation par région — 2019 vs 2024 (Métropole + DROM)",
    width=700,     # largeur totale
    height=900,   # hauteur totale 
    margin=dict(l=20, r=20, t=80, b=40)  # marges compactes
)
fig.show()

Analyse du graphique : géographie du taux de participation en 2019 et 2024

Les cartes mettent en évidence une stabilité générale des taux de participation entre les élections européennes de 2019 et 2024, tout en révélant des écarts territoriaux marqués.

En métropole, la participation demeure globalement comprise entre 45 % et 55 %, avec des taux légèrement plus élevés dans le Grand Ouest (Bretagne, Pays de la Loire) et le Sud-Ouest. À l’inverse, des régions comme le Grand Est ou la Provence-Alpes-Côte d’Azur affichent des niveaux un peu plus faibles. Cette constance relative traduit une mobilisation modérée mais stable des électeurs métropolitains.

On remarque tout de même une légère augmentation des participations entre 2019 et 2024, dans l'ensemble des régions métroppolitaines, mis à part la Corse.

📊 Analyse spatiale :
  • La France métropolitaine conserve une participation moyenne, entre 45 et 55 %.
  • Des contrastes régionaux existent : l’Ouest reste plus mobilisé que le Sud-Est.
  • La stabilité temporelle traduit un ancrage électoral européen relativement constant.

🌍 Focus : les régions et départements d’outre-mer

Dans les DROM, la situation est radicalement différente. Les cartes des Antilles-Guyane et de l’océan Indien affichent des taux de participation très faibles, souvent inférieurs à 30 %, et parfois autour de 10 % en Guyane.

Entre 2019 et 2024, ces territoires ne montrent aucune amélioration significative, confirmant une fracture civique persistante entre la métropole et les outre-mer. Cette faible participation s’explique à la fois par un désintérêt vis-à-vis des enjeux européens et par des facteurs socio-économiques et politiques propres à ces régions.


💬 Conclusion : Ces cartes illustrent une France électorale divisée :
  • Une France métropolitaine modérément mobilisée, où le vote européen conserve un ancrage citoyen.
  • Une France ultramarine largement détachée du scrutin, marquée par un désintérêt électorale européen.

Cette géographie différenciée de la participation montre une division politique observée en 2024, et souligne un investissement électoral croissant entre les régions et à la dynamique européenne et ceux qui s’en sentent exclus.


🗺️ Taux de participation par département : 2019 vs 2024

Cette double carte choroplèthe illustre les taux de participation électorale lors des élections européennes de 2019 et 2024, à l’échelle départementale. Elle met en lumière les différences territoriales de mobilisation électorale à travers l’ensemble du territoire français.

🎯 Objectif :
  • Visualiser la répartition géographique du taux de participation en 2019 et 2024.
  • Mettre en évidence les départements les plus et les moins mobilisés.
  • Comparer visuellement la progression ou la stabilité de la participation selon les territoires.
In [105]:
# --- Initialisation des dataframes et geojson ---
df_dep = assurer_CRS(df_dep)[0]
geojson_dep = assurer_CRS(df_dep)[1]

locations_ids = df_dep["nom_departement_2024"]        # correspondra au "id" du GeoJSON
geojson_dep = json.loads(df_dep.to_json())       # dict GeoJSON (pas une string)
In [106]:
# --- Graphique : Taux de participation par département ---

# --- Sous-graphes ---
fig = make_subplots(
    rows=1, cols=2,
    subplot_titles=["2019", "2024"],
    specs=[[{"type": "choropleth"}, {"type": "choropleth"}]]
)

# --- Traces (même coloraxis partagé) ---
fig.add_trace(
    go.Choropleth(
        geojson=geojson_dep,
        locations=locations_ids,
        z=df_dep["taux_participation_2019"],
        featureidkey="properties.nom_departement_2024",  # clé dans le GeoJSON
        coloraxis="coloraxis",
        customdata=df_dep["nom_departement_2024"],
        hovertemplate="<b>%{customdata}</b><br>Taux de participation: %{z:.1f}%<extra></extra>"
    ),
    row=1, col=1
)

fig.add_trace(
    go.Choropleth(
        geojson=geojson_dep,
        locations=locations_ids,
        z=df_dep["taux_participation_2024"],
        featureidkey="properties.nom_departement_2024",
        coloraxis="coloraxis",
        customdata=df_dep["nom_departement_2024"],
        hovertemplate="<b>%{customdata}</b><br>Taux de participation: %{z:.1f}%<extra></extra>"
    ),
    row=1, col=2
)

# --- Lier chaque trace à sa géo (indispensable en subplots) ---
fig.data[0].geo = "geo"
fig.data[1].geo = "geo2"

# --- Bornes communes & colorbar unique ---
vmin = float(min(df_dep["taux_participation_2019"].min(),
                 df_dep["taux_participation_2024"].min()))
vmax = float(max(df_dep["taux_participation_2019"].max(),
                 df_dep["taux_participation_2024"].max()))

fig.update_layout(
    dragmode=False,       # désactive le drag
    title_text="Taux de participation par département : 2019 vs 2024",
    margin=dict(l=0, r=70, t=60, b=0),
    coloraxis=dict(
        colorscale="Viridis",     # non inversée, lisible daltonisme
        cmin=vmin, cmax=vmax,
        colorbar=dict(
            title="Taux de participation (%)",
            ticksuffix="%",       # si vos valeurs sont en 0–100 ; sinon voir note ci-dessous
            x=1.02, y=0.5, yanchor="middle", len=0.8
        )
    ),
    # --- Définition explicite des géo-axes pour la France métropolitaine ---
    geo=dict(
        scope="europe",
        projection_type="mercator",
        lonaxis_range=[-6, 10],
        lataxis_range=[41, 52],
        showcoastlines=False, showcountries=True, showframe=False,
        bgcolor="rgba(0,0,0,0)"
    ),
    geo2=dict(
        scope="europe",
        projection_type="mercator",
        lonaxis_range=[-6, 10],
        lataxis_range=[41, 52],
        showcoastlines=False, showcountries=True, showframe=False,
        bgcolor="rgba(0,0,0,0)"
    ),
)

fig.show()

Analyse du graphique : évolution du taux de participation par département (2019–2024)

La comparaison des taux de participation départementaux entre 2019 et 2024 révèle une progression générale mais inégale de la mobilisation électorale. Sur la carte de 2024, les nuances de jaune plus soutenu, traduisent une hausse des taux de participation notamment dans plusieurs départements de l’Ouest (Bretagne) et du Sud-Ouest (Occitanie) , où la participation dépasse fréquemment 55 %. Cette évolution contraste avec la carte de 2019, plus uniformément teintée de couleurs intermédiaires, signe d’une mobilisation plus modérée.

📊 Lecture spatiale :
  • Le Sud-Ouest (Nouvelle-Aquitaine, Occitanie) se distingue par une forte participation, souvent supérieure à 55 %, confirmant la vitalité civique de ces territoires, déjà observée lors de précédents scrutins.
  • Le Nord-Est (Hauts-de-France, Grand Est) présente des taux nettement plus faibles, parfois proches de 45 %, traduisant une désaffection persistante dans les zones industrielles et rurales touchées par le désengagement politique.
  • La Bretagne demeure une région particulièrement mobilisée, avec des niveaux de participation supérieurs à ceux de la Normandie, confirmant un ancrage civique historique plus solide à l’Ouest.
  • Les régions du Sud-Est (Provence-Alpes-Côte d’Azur, Corse) ne montrent aucune amélioration notable entre 2019 et 2024 : la participation y reste inférieure à la moyenne nationale, marquée par un vote européen peu mobilisateur.

Cette hétérogénéité traduit une fracture civique territorialisée, où les zones rurales et périurbaines, notamment dans le Nord-Est et le Sud-Est, se mobilisent nettement moins que les régions de l’Ouest et du Sud-Ouest plus participatives. L’Île-de-France et la Normandie se situent dans une position intermédiaire, affichant une participation proche de la moyenne nationale sans réelle progression entre 2019 et 2024.


🌍 Focus : départements et régions d’outre-mer

Les DROM confirment les tendances observées précédemment : la Corse et La Réunion présentent des taux particulièrement faibles, entre 35 % et 40 %. Ce déintérêt durable souligne la distance persistante vis-à-vis du scrutin européen dans ces territoires. Elle reflète des dynamiques socio-économiques et politiques propres aux espaces ultramarins, souvent marqués par un sentiment d’exclusion du débat européen.


💬 Conclusion : L’analyse des cartes départementales montre une amélioration globale de la participation entre 2019 et 2024, mais aussi la persistance de contrastes géographiques profonds. Cette géographie électorale différenciée révèle une “France du vote” : mobilisée, plutôt urbaine et économiquement intégrée ; face à une “France de l’abstention”, concentrée dans les territoires périphériques et DROM. Ce clivage territorial rejoint les grandes lignes de la recomposition politique du scrutin européen et illustre la polarisation croissante entre les espaces connectés à la dynamique européenne et ceux qui s’en sentent exclus.

🏙️ Taux de participation du département d’Île-de-France : 2019 vs 2024

Ce graphique met en évidence l’évolution du taux de participation électorale dans les départements de la région Île-de-France entre les élections européennes de 2019 et de 2024. Il permet d’analyser les différences internes au territoire francilien, marqué par une forte densité urbaine et une diversité sociologique prononcée.

🎯 Objectif :
  • Comparer les taux de participation départementaux franciliens entre 2019 et 2024.
  • Identifier les départements les plus et les moins mobilisés.
  • Observer les écarts entre zones urbaines et périurbaines de la région capitale.
In [107]:
# --- Graphique : Taux de participation - Île de France ---

# --- Sélection des départements d’Île-de-France ---
idf_codes = ["75", "77", "78", "91", "92", "93", "94", "95"]
df_idf = df_dep[df_dep["code_departement"].isin(idf_codes)].copy()

# --- Vérification du CRS (lon/lat WGS84) ---
if getattr(df_idf, "crs", None) is not None:
    try:
        if df_idf.crs.to_epsg() != 4326:
            df_idf = df_idf.to_crs(epsg=4326)
    except Exception:
        df_idf = df_idf.to_crs(epsg=4326)

# --- Création du GeoJSON limité à l’Île-de-France ---
df_idf = df_idf.reset_index(drop=True)
locations_ids = df_idf["nom_departement_2024"]
geojson_idf = json.loads(df_idf.to_json())

# --- Sous-graphes 2019 / 2024 ---
fig = make_subplots(
    rows=1, cols=2,
    subplot_titles=["2019", "2024"],
    specs=[[{"type": "choropleth"}, {"type": "choropleth"}]]
)

# --- Ajout des 2 cartes ---
fig.add_trace(
    go.Choropleth(
        geojson=geojson_idf,
        locations=locations_ids,
        z=df_idf["taux_participation_2019"],
        featureidkey="properties.nom_departement_2024",
        coloraxis="coloraxis",
        customdata=df_idf["nom_departement_2024"],
        hovertemplate="<b>%{customdata}</b><br>Taux de participation: %{z:.1f}%<extra></extra>"
    ),
    row=1, col=1
)

fig.add_trace(
    go.Choropleth(
        geojson=geojson_idf,
        locations=locations_ids,
        z=df_idf["taux_participation_2024"],
        featureidkey="properties.nom_departement_2024",
        coloraxis="coloraxis",
        customdata=df_idf["nom_departement_2024"],
        hovertemplate="<b>%{customdata}</b><br>Taux de participation: %{z:.1f}%<extra></extra>"
    ),
    row=1, col=2
)

# --- Lier chaque trace à sa géo ---
fig.data[0].geo = "geo"
fig.data[1].geo = "geo2"

# --- Échelle commune ---
vmin = float(min(df_idf["taux_participation_2019"].min(),
                 df_idf["taux_participation_2024"].min()))
vmax = float(max(df_idf["taux_participation_2019"].max(),
                 df_idf["taux_participation_2024"].max()))

# --- Mise en forme & zoom sur l’Île-de-France ---
fig.update_layout(
    dragmode=False,       # désactive le drag
    title_text="Taux de participation — Île-de-France (2019 vs 2024)",
    margin=dict(l=0, r=70, t=60, b=0),
    coloraxis=dict(
        colorscale="Viridis",
        cmin=vmin, cmax=vmax,
        colorbar=dict(
            title="Taux de participation (%)",
            ticksuffix="%",
            x=1.02, y=0.5, yanchor="middle", len=0.8
        )
    ),
    geo=dict(
        projection_type="mercator",
        center=dict(lon=2.35, lat=48.85),  # centré sur Paris
        lonaxis_range=[1.5, 3.5],          # zoom longitude
        lataxis_range=[48, 49.5],          # zoom latitude
        showcoastlines=False, showcountries=False, showframe=False,
        bgcolor="rgba(0,0,0,0)"
    ),
    geo2=dict(
        projection_type="mercator",
        center=dict(lon=2.35, lat=48.85),
        lonaxis_range=[1.5, 3.5],
        lataxis_range=[48, 49.5],
        showcoastlines=False, showcountries=False, showframe=False,
        bgcolor="rgba(0,0,0,0)"
    ),
)

fig.show()

Analyse du graphique : évolution du taux de participation en Île-de-France (2019–2024)

La comparaison des taux de participation départementaux en Île-de-France entre 2019 et 2024 révèle une légère hausse de la mobilisation électorale, mais aussi la persistance de fortes disparités internes au sein de la région capitale. Les données montrent que les départements les plus centraux et urbanisés, notamment Paris et les Hauts-de-Seine affichent les taux de participation les plus élevés, dépassant 55 % en 2024, soit une progression notable par rapport à 2019.

📊 Lecture régionale :
  • Paris et les Hauts-de-Seine : participation élevée (>55 %) en 2024, hausse significative depuis 2019.
  • Le Val-de-Marne, l'Essonne et les Yvelines : niveaux intermédiaires autour de 50 %.
  • La Seine-Saint-Denis et le Val-d’Oise: participation plus faible (< 45%), marquant un désengagement durable.

Ces contrastes illustrent une fracture civique intra-régionale, où la géographie montre directement les différences socio-économiques entre l'Ouest de l'Île-de-France, Paris inclus et l'Est de l'Île-de-France.


🏙️ Analyse socio-territoriale :

Les zones les plus aisées et centrales se montrent davantage investies dans le scrutin européen, tandis que les espaces périurbains et populaires présentent une abstention structurelle plus forte. Cette géographie électorale reflète les différences sociales et territoriales propre à la région francilienne : proximité des institutions, niveau d’éducation, stabilité socio-professionnelle et sentiment d’intégration à l’Europe.

Les départements de la grande couronne, souvent éloignés des centres décisionnels et marqués par des conditions socio-économiques plus fragiles, restent moins mobilisés, traduisant une fracture civique persistante.


💬 Conclusion : L’Île-de-France illustre à elle seule la fracture civique française : une mobilisation métropolitaine, instruite et intégrée à l’espace européen, face à une abstention périphérique et populaire. L’évolution entre 2019 et 2024 traduit une remobilisation relative, sans inversion de tendance structurelle : la région reste un territoire de participation inégale, reflet des disparités sociales et territoriales françaises.

🧩 Transformation des données : mise au format long

Avant toute visualisation, les données issues des élections de 2019 et 2024 sont converties dans un format long adapté aux analyses comparatives et aux graphiques Plotly. Cette étape permet d’unifier les structures de données des régions et des départements, en rassemblant les voix de chaque liste dans une table unique, où chaque ligne correspond à une combinaison (territoire, liste, année, voix).

🎯 Objectif :
  • Transformer les bases régionales et départementales en tables longues.
  • Associer à chaque ligne une année (2019 ou 2024), une liste électorale et un nombre de voix.
  • Ajouter une abréviation standardisée pour chaque liste (ex. RN, LFI, LR, etc.) afin de faciliter la lecture graphique.

In [108]:
# --- Format long & sigles politiques --- 

# --- Table longue régions 2024 & 2019
df_melt_reg24 = transformer_voix_listes(
    df_reg, 
    id_vars=["code_region","nom_region_2024"]
)
df_melt_reg19 = transformer_voix_listes(
    df_reg, 
    id_vars=["code_region","nom_region_2024"]
)

# --- Table longue sur 2019 & 2024 des régions
df_long_reg = transformer_voix_listes(
    df_reg,
    id_vars=["code_region", "nom_region_2019", "nom_region_2024"]
)
df_long_reg = df_long_reg[df_long_reg["annee"].isin(["2019", "2024"])].copy()

# Nom abrégé (sigle politique)
df_long_reg["liste abrégée"] = df_long_reg["liste"].map(lambda x: nom_court(x, 22))

# --- Table longue sur 2019 & 2024 des départements
df_long_dep = transformer_voix_listes(
    df_dep,
    id_vars=["code_departement", "nom_departement_2019", "nom_departement_2024"]
)
df_long_dep = df_long_dep[df_long_dep["annee"].isin(["2019", "2024"])].copy()

# Nom abrégé (sigle politique)
df_long_dep["liste abrégée"] = df_long_dep["liste"].map(lambda x: nom_court(x, 22))

🗳️ Répartition des voix par liste — Élections européennes 2019 vs 2024

Ce graphique présente la répartition des suffrages exprimés entre les principales listes électorales françaises lors des élections européennes de 2019 et 2024. Il permet de visualiser les évolutions des principales forces politiques au niveau national, ainsi que les dynamiques de progression ou de recul de chaque groupe.

🎯 Objectif :
  • Comparer les parts de voix obtenues par les principales listes entre 2019 et 2024.
  • Identifier les gains et pertes électoraux des grandes formations politiques.
  • Visualiser la recomposition des forces électorales françaises à travers le scrutin européen.
In [109]:
# --- Calcul des principales listes electorales ---

# --- Top listes calculé sur les DEUX années (pour cohérence des couleurs)
TOP_N = 4
top_2019 = (
    df_long_reg[df_long_reg["annee"] == "2019"]
    .groupby("liste", as_index=False)["voix"]
    .sum()
    .sort_values("voix", ascending=False)
    .head(TOP_N)["liste"]
)

top_2024 = (
    df_long_reg[df_long_reg["annee"] == "2024"]
    .groupby("liste", as_index=False)["voix"]
    .sum()
    .sort_values("voix", ascending=False)
    .head(TOP_N)["liste"]
)

top_listes = set(top_2019).union(set(top_2024))
In [110]:
# --- Graphique des votes exprimés par liste électorale ---

# --- Conserver le Top N et regrouper le reste en "Autres"
df_long_reg["liste_top"] = np.where(df_long_reg["liste"].isin(top_listes), df_long_reg["liste"], "Autres")
df_long_reg["liste abrégée"] = np.where(
    df_long_reg["liste"].isin(top_listes), df_long_reg["liste abrégée"], "Autres"
)

# Ordre de la légende (Top N par poids total, puis "Autres")
order_legende = (
    df_long_reg.groupby("liste abrégée")["voix"]
    .sum()
    .sort_values(ascending=False)
    .index.tolist()
)

if "Autres" in order_legende:
    order_legende = [x for x in order_legende if x != "Autres"] + ["Autres"]

# --- Calcul du total de voix par région (toutes années)
ordre_regions = (
    df_long_reg.groupby("nom_region_2024")["voix"]
    .sum()
    .sort_values(ascending=False)
    .index.tolist()
)
# --- Couleurs cohérentes avec le mapping politique
# Utilisation directe de ton dictionnaire COULEURS_LISTES
# On ne garde que les couleurs présentes + un gris pour "Autres"
couleurs_listes = {
    k: v for k, v in COULEURS_LISTES.items() if k in order_legende
}
couleurs_listes["Autres"] = "#BDBDBD"  # gris neutre

# --- Figure à deux panneaux (facettes) avec même légende et couleurs
fig = px.bar(
    df_long_reg,
    x="nom_region_2024",
    y="voix",
    color="liste abrégée",
    facet_col="annee",  # 2 panneaux côte à côte
    category_orders={
        "nom_region_2024": ordre_regions,
        "liste abrégée": order_legende,
        "annee": ["2019", "2024"]
    },
    color_discrete_map=couleurs_listes,
    barmode="stack",
    title="Répartition et évolution des principales forces politiques par région (2019–2024)",
    labels={
        "voix": "Nombre de voix",
        "nom_region_2024": "Région",
        "liste abregée": "Listes"
    }
)

# Nettoyer les titres de facettes
fig.for_each_annotation(lambda a: a.update(text=a.text.replace("annee=", "")))

# Mise en forme générale
fig.update_yaxes(matches="y")
fig.update_xaxes(tickangle=45)
fig.update_layout(
    legend_title_text="Listes électorales",
    bargap=0.15)

fig.show()

Analyse du graphique : répartition des voix par région — 2019 vs 2024

Le graphique met en évidence une évolution nette de la hiérarchie électorale entre 2019 et 2024, marquée par un augmentation importante du Rassemblement national (RN) et un recul significatif de la majorité présidentielle (RE). Cette recomposition confirme la fracture croissante du paysage politique français à l’échelle régionale.

🟦 Bloc national-populiste (RN) :

En 2019, le RN dominait déjà dans plusieurs régions, en particulier dans le Nord, l’Est et le Sud, mais la différence demeurait encore limitée face à Renaissance. En 2024, l’écart se creuse nettement dans presque toutes les régions, faisant du RN la première force politique sur la quasi-totalité du territoire métropolitain. Seules quelques régions urbaines et universitaires (Île-de-France, Auvergne-Rhône-Alpes, Occitanie) présentent une résistance électorale plus marquée.


🟨 Bloc central et progressiste (Renaissance / RE) :

La liste Renaissance, majoritaire en 2019 dans plusieurs régions (Île-de-France, Bretagne, Pays de la Loire), enregistre en 2024 un recul marqué, dans l'ensemble des régions. Cette diminution reflète un affaiblissement du soutien électoral à la majorité présidentielle, marqué par un affaiblissement du lien électora des classes moyennes et rurales envers le projet européen défendu par le centre.

>

🟥 Bloc de gauche (PS, LFI, EELV) :

La gauche retrouve une certaine vitalité en 2024, mais demeure fragmentée :

  • PS → regain notable dans l'ensemble des régions comparés à 2019.
  • LFI → progression dans les grandes métropoles et les départements populaires.
  • EELV → enregistre un recul important après son pic de 2019, potentiellement lié à un essouflement relatif de la dynamique du parti écologiste.

Cette diversité des dynamiques témoigne d’un rééquilibrage à gauche, mais sans réelle unité électorale.


💬 Conclusion :

La comparaison 2019 et 2024 révèle une disparité du paysage politique autour de plusieurs sujets :

  • Domination du RN : le Rassemblement national s'impose désormais comme la première force politique, au-delà de ses bastions historiques.
  • Effondrement du centre : Renaissance subit un recul historique sur l'ensemble du territoire, révélant une crise de confiance envers la majorité présidentielle et une rupture électoraleavec les classes moyennes et les territoires ruraux.
  • Gauche fragmentée : malgré un regain d'influence du PS et de LFI dans certaines zones urbaines et populaires, la gauche reste divisée et peine à constituer un bloc unifié capable de rivaliser avec le RN.
  • Recul écologiste : EELV enregistre un net recul après son pic de 2019, suggérant un affaiblissement de la dynamique écologiste face aux enjeux sécuritaires et économiques dominants.
  • Ces évolutions révèlent une fracture territoriale croissante : la France électorale se divise entre les périphéries, qui votent massivement RN en quête de protection, et les grandes villes, plus favorables au projet européen et à l'ouverture.


    🏆 Identification des listes arrivées en tête — Régions et Départements (2019 & 2024)

    Cette étape vise à déterminer, pour chaque région et chaque département, la liste électorale arrivée en tête lors des élections européennes de 2019 et de 2024. L’objectif est d’identifier les dynamiques territoriales dominantes et de repérer les zones de force des principaux partis.

    🎯 Objectif :
    • Déterminer automatiquement la liste gagnante (ayant obtenu le plus de voix) pour chaque région et département.
    • Créer deux tableaux récapitulatifs distincts : un pour les régions (df_tetes) et un pour les départements (df_tetes_dep).
    • Préparer ces données pour les futures cartes électorales et visualisations comparatives.
    In [111]:
    # --- Liste électorale gagnante ---
    
    # Gagnant par région / année
    idx_max_reg = df_long_reg.groupby(["annee", "code_region"])["voix"].idxmax()
    df_tetes = (df_long_reg.loc[idx_max_reg, ["annee", "code_region", "nom_region_2024", "liste abrégée", "liste","voix"]]
                       .sort_values(["annee", "nom_region_2024"])
                       .rename(columns={"liste abrégée": "liste_en_tete", "voix": "voix_en_tete"}))
    
    # Gagnant par département / année
    idx_max_dep = df_long_dep.groupby(["annee", "code_departement"])["voix"].idxmax()
    df_tetes_dep = (df_long_dep.loc[idx_max_dep, ["annee", "code_departement", "nom_departement_2024", "liste abrégée", "liste","voix"]]
                       .sort_values(["annee", "nom_departement_2024"])
                       .rename(columns={"liste abrégée": "liste_en_tete", "voix": "voix_en_tete"}))
    

    🗺️ Cartes des listes arrivées en tête par région — 2019 vs 2024

    Cette visualisation présente la carte électorale régionale de la France pour les élections européennes de 2019 et 2024. Les six cartes indiquent, pour chaque territoire, la liste arrivée en première position — en métropole, aux Antilles-Guyane et dans l'océan Indien. Cela permet d'avoir une vision complète de l'évolution politique entre les deux scrutins.

    🎯 Objectif :
    • Comparer la répartition territoriale des listes gagnantes entre 2019 et 2024.
    • Visualiser bascule politique dans plusieurs régions métropolitaines.
    • Identifier la diversité électorale des régions.
    In [112]:
    # --- Fonction utilitaire + initialisation couleur/parti ---
    
    # --- Fonction pour filtrer le GeoDataFrame et générer un GeoJSON Plotly-compatible ---
    def geojson_par_zone_annee(gdf_geo, df_attr, zone, annee):
        # géométrie filtrée (par codes DOM/Métropole)
        g = filtre_zone(gdf_geo, zone, "code_region").copy()
        # attributs pour l'année
        attrs = (
            df_attr[df_attr["annee"] == annee]
            .loc[:, ["code_region", "liste_en_tete", "nom_region_2024"]]
            .drop_duplicates("code_region")
        )
        # jointure géo + attributs
        g = g.merge(attrs, on="code_region", how="left")
        # retourne un FeatureCollection utilisable par Plotly
        return json.loads(g.to_json())
    
    # --- Créer la palette à partir de COULEURS_LISTES ---
    partis = list(COULEURS_LISTES.keys())
    palette = [COULEURS_LISTES[p] for p in partis]
    color_scale = [[i / (len(palette) - 1), palette[i]] for i in range(len(palette))]
    
    In [113]:
    # --- Graphique : listes arrivées en têtes par région ---
    
    # --- Figure 3x2 ---
    fig = make_subplots(
        rows=3, cols=2,
        subplot_titles=[
            "Métropole 2019", "Métropole 2024",
            "Antilles-Guyane 2019", "Antilles-Guyane 2024",
            "Océan Indien 2019", "Océan Indien 2024",
        ],
        specs=[[{"type": "choropleth"}, {"type": "choropleth"}]] * 3,
        horizontal_spacing=0.03,
        vertical_spacing=0.07,
    )
    
    # --- Générer tous les GeoJSONs possibles ---
    geojson_dict = {}
    for zone in ["metropole", "antilles", "indien"]:
        for annee in ["2019", "2024"]:
            geojson_dict[(zone, annee)] = geojson_par_zone_annee(df_reg, df_tetes, zone, annee)
    
    # --- Boucle zones × années ---
    
    for row, (zone, _, center) in enumerate(zones, start=1):
        for col, annee in enumerate(["2019", "2024"], start=1):
            gj = geojson_dict[(zone, annee)]
            if not isinstance(gj, dict) or "features" not in gj or len(gj["features"]) == 0:
                continue
    
            # Sous-ensemble attributaire aligné sur le GeoJSON
            codes_geo = {f["properties"]["code_region"] for f in gj["features"]}
            df_zone = filtre_zone(df_tetes, zone, "code_region")
            df_zone = df_zone[(df_zone["annee"] == annee) & (df_zone["code_region"].isin(codes_geo))].copy()
            if df_zone.empty:
                continue
    
            z_values = df_zone["liste_en_tete"].map(
                lambda x: partis.index(x) if x in partis else len(palette) - 1
            )
    
            fig.add_trace(
                go.Choropleth(
                    geojson=gj,
                    locations=df_zone["code_region"],
                    z=z_values,
                    featureidkey="properties.code_region",
                    colorscale=color_scale,
                    zmin=0, zmax=len(palette) - 1,
                    showscale=False,
                    text=df_zone["nom_region_2024"] + "<br><b>" + df_zone["liste_en_tete"] + "</b>",
                    hovertemplate="<b>%{text}</b><extra></extra>",
                    marker_line_width=0.4,
                    marker_line_color="white",
                ),
                row=row, col=col,
            )
    
    # --- Lier les géos ---
    geo_names = [f"geo{i}" for i in range(1, 7)]
    for idx, geo in enumerate(geo_names[:len(fig.data)]):
        fig.data[idx].geo = geo
    
    # --- Mise en page et centrage ---
    
    fig.update_layout(
        dragmode=False,       # désactive le drag
        title_text="Listes arrivées en tête par région — 2019 vs 2024",
        title_x=0.5,
        height=1100,
        width=900,
        margin=dict(l=10, r=10, t=60, b=10),
        
    
        geo=dict(center={"lat": 46.6, "lon": 2.5}, projection_type="mercator",
                 lonaxis_range=[-6, 10], lataxis_range=[41, 52], showcoastlines=True, showcountries=True,fitbounds="locations"),
        geo2=dict(center={"lat": 46.6, "lon": 2.5}, projection_type="mercator",
                  lonaxis_range=[-6, 10], lataxis_range=[41, 52], showcoastlines=True, showcountries=True,fitbounds="locations"),
    
        geo3=dict(center={"lat": 12, "lon": -61}, projection_type="mercator",
                  lonaxis_range=[-67, -49], lataxis_range=[4, 18],showcoastlines=True, showcountries=True,fitbounds="locations"),
        geo4=dict(center={"lat": 12, "lon": -61}, projection_type="mercator",
                  lonaxis_range=[-67, -49], lataxis_range=[4, 18],showcoastlines=True, showcountries=True, fitbounds="locations"),
    
        geo5=dict(center={"lat": -18, "lon": 52},projection_type="mercator",
                  lonaxis_range=[43, 58],lataxis_range=[-23, -10], showcoastlines=True,showcountries=True,fitbounds="locations"),
        geo6=dict(center={"lat": -18, "lon": 52},projection_type="mercator",
                  lonaxis_range=[43, 58],lataxis_range=[-23, -10],showcoastlines=True,showcountries=True, fitbounds="locations"),
    )
        
    # --- Nettoyage visuel ---
    for axis in fig.layout:
        if axis.startswith("xaxis") or axis.startswith("yaxis"):
            fig.layout[axis].visible = False
    
    # on garde les 15 partis les plus représentés pour la lisibilité
    top_partis = df_tetes["liste_en_tete"].value_counts().head(5).index
    for i, parti in enumerate(top_partis):
        fig.add_trace(go.Scatter(
            x=[None], y=[None],
            mode="markers",
            marker=dict(size=10, color=COULEURS_LISTES.get(parti, "#BDBDBD")),
            legendgroup=parti,
            showlegend=True,
            name=parti
        ))
    fig.update_layout(
        title_text="Listes arrivées en tête par région — 2019 vs 2024",
        title_x=0.5,
        width=700,     # largeur totale
        height=900,   # hauteur totale 
    
        # Marge supérieure plus grande pour libérer l'espace entre le titre et les graphiques
        margin=dict(l=10, r=10, t=120, b=10),
    
        # --- Légende sous le titre, mais au-dessus des graphiques ---
        legend=dict(
            title_text="Listes électorales",
            orientation="h",          # horizontale
            x=0.5,                    # centrée horizontalement
            xanchor="center",
            y=1.07,                   # position verticale : légèrement sous le titre
            yanchor="top",            # ancrée par le haut
            font=dict(size=12),
            title_font=dict(size=13)
        ),
    
        legend_title_font=dict(size=13, color="black"),
        paper_bgcolor="white",
        plot_bgcolor="white",
    )
    fig.show()
    

    Analyse du graphique : listes arrivées en tête par région — 2019 vs 2024

    Ces cartes permettent de visualiser la distribution territoriale du vote majoritaire lors des élections européennes de 2019 et 2024, en distinguant les régions métropolitaines et DROM. L’évolution est nette : entre les deux scrutins, le Rassemblement national (RN), représenté en bleu foncé, a considérablement renforcé sa position, passant d'une domination partielle à une domination quasi totale sur le territoire national.

    🗺️ 2019 : une France encore partagée
    • 🔵 Le Rassemblement national (RN) arrive en tête dans une large partie du territoire (Sud, Est et Nord), confirmant son ancrage dans les espaces ruraux et périurbains.
    • 🟡 La liste Renaissance (RE), en jaune, s’impose dans plusieurs régions métropolitaines, notamment dans l’Ouest (Bretagne, Pays de la Loire) et le Centre.

    La carte de 2019 révèle ainsi une fragmentation électorale, où le vote RN s’oppose à un pôle centriste encore solide.


    📈 2024 : généralisation territoriale du vote RN

    En 2024, la situation change radicalement : le RN s’impose dans la totalité des régions métropolitaines, y compris celles où il était auparavant minoritaire. Cette progression traduit une extension sociologique et géographique de son électorat, au-delà de ses bastions traditionnels du Nord-Est et du Midi.

    Le vote centriste et pro-européen de Renaissance s’effondre, dans l'ensemble des régions, tandis que la gauche demeure marginale, concentrée dans les grandes villes seulement.


    🌍 Focus : DROM
    • En 2019, certaines zones (notamment Antilles et Guyane) affichaient encore une pluralité électorale avec des succès ponctuels de Renaissance.
    • En 2024, le RN domine également dans ces territoires, à l’exception de la Martinique, où La France insoumise (LFI), en rouge, arrive en tête, signe d’une spécificité politique marquée par une défiance vis-à-vis des partis de droite.
    • Dans l’océan Indien (La Réunion, Mayotte), le RN conserve son implantation stable sur les deux scrutins, traduisant la durabilité de sa présence électorale dans les territoires éloignés de la métropole.

    💬 Conclusion :

    Ces cartes illustrent une reconfiguration majeure du paysage politique français :

    • La France, autrefois partagée entre plusieurs blocs régionaux, est désormais unifiée autour d’un vote RN majoritaire.
    • Le centre pro-européen s'efface, et la gauche reste fragmentée.
    • Le vote RN devient un vote national transversal, touchant aussi bien les zones rurales que périurbaines, et jusqu’à certains territoires d'outre-mer.

    Ce basculement marque une rupture politique et territoriale inédite dans l'histoire électorale européenne récente. La France électorale se divise désormais selon un fossé profond opposant centres et périphéries, mêlant enjeux sociaux et politiques.


    🗺️ Cartes des têtes de listes par département — Île-de-France (2019 vs 2024)

    Cette visualisation compare la répartition des listes arrivées en tête dans les départements franciliens entre les élections européennes de 2019 et 2024. Elle met en évidence les dynamiques internes à la région capitale, marquées par de fortes disparités électorales entre le cœur urbain et la périphérie.

    🎯 Objectif :
    • Identifier la liste électorale majoritaire dans chaque département francilien.
    • Comparer les changements de domination politique entre 2019 et 2024.
    • Mettre en évidence la polarisation territoriale du vote francilien entre zones centrales et périphériques.
    In [114]:
    # --- Graphique : Listes arrivées en têtes - Île-de-France ----
    
    # --- Codes départements Île-de-France 
    idf_codes = ["75", "77", "78", "91", "92", "93", "94", "95"]
    
    # --- Filtrage du GeoDataFrame départemental ---
    gdf_idf = df_dep[df_dep["code_departement"].isin(idf_codes)].copy()
    
    # --- Fusion des têtes de listes pour 2019 et 2024
    # Si ton DataFrame d’entrée s’appelle df_tetes_dep
    df_idf_tetes = df_tetes_dep[df_tetes_dep["code_departement"].isin(idf_codes)].copy()
    
    # --- Créer la palette de couleurs (comme avant)
    partis = list(COULEURS_LISTES.keys())
    palette = [COULEURS_LISTES[p] for p in partis]
    color_scale = [[i / (len(palette) - 1), palette[i]] for i in range(len(palette))]
    
    # --- Préparation des GeoJSON par année
    geojson_idf_2019 = json.loads(
        gdf_idf.merge(
            df_idf_tetes[df_idf_tetes["annee"] == "2019"]
            .loc[:, ["nom_departement_2024", "liste_en_tete"]]
            .drop_duplicates("nom_departement_2024"),
            on="nom_departement_2024",
            how="left",
        ).to_json()
    )
    
    geojson_idf_2024 = json.loads(
        gdf_idf.merge(
            df_idf_tetes[df_idf_tetes["annee"] == "2024"]
            .loc[:, ["nom_departement_2024", "liste_en_tete"]]
            .drop_duplicates("nom_departement_2024"),
            on="nom_departement_2024",
            how="left",
        ).to_json()
    )
    
    # --- Figure 1x2 ---
    fig = make_subplots(
        rows=1, cols=2,
        subplot_titles=["2019", "2024"],
        specs=[[{"type": "choropleth"}, {"type": "choropleth"}]],
    )
    
    # --- Carte 2019 ---
    z2019 = [
        partis.index(x) if x in partis else len(palette) - 1
        for x in df_idf_tetes[df_idf_tetes["annee"] == "2019"]["liste_en_tete"]
    ]
    fig.add_trace(
        go.Choropleth(
            geojson=geojson_idf_2019,
            locations=df_idf_tetes[df_idf_tetes["annee"] == "2019"]["nom_departement_2024"],
            z=z2019,
            featureidkey="properties.nom_departement_2024",
            colorscale=color_scale,
            zmin=0, zmax=len(palette) - 1,
            showscale=False,
            text=df_idf_tetes[df_idf_tetes["annee"] == "2019"]["liste_en_tete"],
            customdata=df_idf_tetes[df_idf_tetes["annee"] == "2019"]["nom_departement_2024"],
            hovertemplate="<b>%{customdata}</b><br>%{text}<extra></extra>",
            marker_line_color="white",
            marker_line_width=0.5,
        ),
        row=1, col=1,
    )
    
    # --- Carte 2024 ---
    z2024 = [
        partis.index(x) if x in partis else len(palette) - 1
        for x in df_idf_tetes[df_idf_tetes["annee"] == "2024"]["liste_en_tete"]
    ]
    fig.add_trace(
        go.Choropleth(
            geojson=geojson_idf_2024,
            locations=df_idf_tetes[df_idf_tetes["annee"] == "2024"]["nom_departement_2024"],
            z=z2024,
            featureidkey="properties.nom_departement_2024",
            colorscale=color_scale,
            zmin=0, zmax=len(palette) - 1,
            showscale=False,
            text=df_idf_tetes[df_idf_tetes["annee"] == "2024"]["liste_en_tete"],
            customdata=df_idf_tetes[df_idf_tetes["annee"] == "2019"]["nom_departement_2024"],
            hovertemplate="<b>%{customdata}</b><br>%{text}<extra></extra>",
            marker_line_color="white",
            marker_line_width=0.5,
        ),
        row=1, col=2,
    )
    
    # ---  Positionner correctement l’Île-de-France ---
    fig.data[0].geo = "geo"
    fig.data[1].geo = "geo2"
    # --- Légende personnalisée avec les partis et leurs vraies couleurs ---
    idf_partis = sorted(df_idf_tetes["liste_en_tete"].unique())
    for parti in idf_partis:
        fig.add_trace(go.Scatter(
            x=[None], y=[None],
            mode="markers",
            marker=dict(size=10, color=COULEURS_LISTES.get(parti, "#BDBDBD")),
            name=parti,
            showlegend=True
        ))
    
    fig.update_layout(
        dragmode=False,       # désactive le drag
        title_text="Têtes de listes par département — Île-de-France (2019 vs 2024)",
        margin=dict(l=0, r=70, t=60, b=0),
        geo=dict(
            projection_type="mercator",
            center=dict(lon=2.35, lat=48.85),
            lonaxis_range=[1.5, 3.5],
            lataxis_range=[48, 49.5],
            showcoastlines=False,
            showcountries=False,
            showframe=False,
            bgcolor="rgba(0,0,0,0)",
        ),
        geo2=dict(
            projection_type="mercator",
            center=dict(lon=2.35, lat=48.85),
            lonaxis_range=[1.5, 3.5],
            lataxis_range=[48, 49.5],
            showcoastlines=False,
            showcountries=False,
            showframe=False,
            bgcolor="rgba(0,0,0,0)",
        ),
        legend=dict(
            title_text="Listes électorales",
            orientation="v",      # verticale
            x=1.05,               # légèrement à droite du graphe
            xanchor="left",
            y=0.5,
            yanchor="middle",
            bgcolor="rgba(255,255,255,0.8)",  # fond blanc semi-transparent
            bordercolor="rgba(0,0,0,0.2)",
            borderwidth=1,
            font=dict(size=12),
            title_font=dict(size=13)
        ),
        template="plotly_white"
    )
    
    # --- Nettoyage visuel complet ---
    fig.update_xaxes(visible=False, showgrid=False)
    fig.update_yaxes(visible=False, showgrid=False)
    
    fig.update_layout(
        plot_bgcolor="white",     # fond blanc propre
        paper_bgcolor="white",    # fond du canevas blanc aussi
    )
    fig.show()
    

    Analyse du graphique : têtes de liste par département — Île-de-France (2019 vs 2024)

    Les cartes comparent la répartition des listes arrivées en tête dans les départements d'Île-de-France entre 2019 et 2024, mettant en évidence une recomposition politique majeure dans la région capitale. L’évolution du vote illustre, à une échelle condensée, les dynamiques nationales de fragmentatioin électorale et sociale.

    🗳️ 2019 : un bastion centriste et pro-européen
    • 🟡 La quasi-totalité des départements place la liste Renaissance (RE) en tête (en jaune), témoignant d’un ancrage fort du vote macroniste dans la région parisienne.
    • 🔵 Seul le Seine-et-Marne, plus rural et périphérique, accorde la première place au Rassemblement national (RN), premier signe d’un basculement territorial à venir.
    • Ce résultat reflète la sociologie urbaine de l’Île-de-France, où les électeurs soutiennent majoritairement le projet centriste et pro-UE.

    📊 2024 : une recomposition profonde et contrastée

    En 2024, le paysage d'Île-de-France se transforme radicalement :

    • Le Rassemblement national (RN), en bleu, progresse massivement et arrive en tête dans la majorité des départements, notamment ceux de la grande couronne (Seine-et-Marne, Essonne, Val-d’Oise), confirmant son ancrage dans les zones périurbaines et populaires.
    • Le centre parisien et la petite couronne se distinguent par une plus grande diversité politique :
      • Paris place en tête le Parti socialiste (PS) (rose), symbole du retour d’un vote social-démocrate urbain et modéré.
      • Le Seine-Saint-Denis vote majoritairement pour La France insoumise (LFI) (rouge), fidèle à son profil populaire et militant.
      • Les Hauts-de-Seine et le Val-de-Marne conservent une présence relative de Renaissance, sans toutefois dominer la région.

    📍 Lecture géopolitique : une fracturation métropolitaine
    • Les centres urbains denses (Paris, petite couronne) tendent vers la gauche progressiste et sociale, attachée à l’Union européenne et aux valeurs métropolitaines.
    • La grande couronne, marquée par la précarité et l’éloignement des centres décisionnels, exprime un vote protestataire incarné par le RN.
    • Cette opposition interne traduit un clivage socio-territorial fort entre le cœur métropolitain intégré et les périphéries en tension.

    💬 Conclusion :

    L’Île-de-France, autrefois bastion du vote centriste et pro-européen, devient en 2024 un véritable microcosme du clivage national :

    • Une France des métropoles, tournée vers la gauche et l’Europe, concentrée dans le cœur urbain.
    • Une France périphérique, socialement fragilisée, où le vote RN s’impose comme principal vecteur de contestation.

    Cette polarisation interne à la région capitale illustre la reconfiguration du vote européen autour d’un nouvel axe : la tension entre intégration européenne et détachement socio-économique.


    📘 Conclusion générale : une recomposition électorale et territoriale profonde entre 2019 et 2024

    L’étude de l’évolution de la géographie électorale française entre les élections européennes de 2019 et 2024 met en évidence une double dynamique : politique et territoriale qui redéfinit durablement les équilibres électoraux du pays.


    📊 Participation électorale : une remobilisation inégale

    Les données révèlent une légère remobilisation électorale en 2024, mais cette progression reste territorialement contrastée :

    • Les régions urbaines et métropolitaines enregistrent une participation supérieure à la moyenne, portée par des électeurs plus insérés dans les dynamiques économiques et européennes.
    • À l’inverse, les territoires ruraux, périurbains et d'outre-mer demeurent marqués par une abstention structurelle élevée.

    Cette géographie de la mobilisation traduit une fracture civique persistante, opposant les espaces connectés et intégrés à l’Europe à ceux qui s’en sentent exclus.


    🗳️ Recomposition politique : domination du RN et recul du centre

    Sur le plan politique, la comparaison entre 2019 et 2024 montre une restructuration profonde du paysage électoral :

    • Le Rassemblement national (RN), déjà fort en 2019, devient en 2024 la première force politique nationale, dominant la quasi-totalité des régions françaises.
    • Son implantation s’est étendue à des territoires auparavant modérés, notamment dans l’Ouest et le Centre, démontrant un vote de contestation généralisé.
    • La majorité présidentielle (Renaissance) subit un recul net, perdant son statut de pivot politique et ses bastions métropolitains.

    🔴 La gauche en recomposition : renouveau social-démocrate et ancrage populaire

    • Le Parti socialiste (PS) retrouve une influence régionale, notamment à Paris, grâce à un positionnement social-démocrate clarifié.
    • La France insoumise (LFI) s’impose dans les grands pôles urbains et populaires, ainsi que dans certains territoires d'outre-mer.
    • Ces dynamiques traduisent un retour d'une division sociale et territoriale au cœur du vote européen.

    🏙️ L’Île-de-France : miroir du clivage national

    L’analyse spécifique de l’Île-de-France illustre parfaitement cette fracture :

    • La capitale et la petite couronne s’ancrent à gauche, entre vote social-démocrate (PS) et militant (LFI).
    • La grande couronne, plus populaire et éloignée du centre, se tourne vers le RN, traduisant un vote de rupture.

    La région devient ainsi un microcosme du clivage national : métropoles progressistes contre périphéries contestataires.


    ⚖️ Une opposition marquée du paysage électoral

    Entre 2019 et 2024, la France passe d’un système dominé par deux principales forces : la liste Renaissance et le Rassemblement national (RN), à une situation où le RN s’impose comme la première force dans la quasi-totalité des régions.

    Toutefois, les grandes métropoles conservent un profil distinct : elles votent majoritairement à gauche, en faveur des listes socialistes, écologistes ou insoumises, tandis que les territoires périphériques et ruraux se tournent davantage vers le RN.

    Cette évolution met en évidence une reconfiguration territoriale du vote : la France urbaine et connectée se distingue de la France périphérique, selon des dynamiques liées aux conditions socio-économiques et au rapport différencié au projet européen.